/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright Blazebit
 */

package com.blazebit.persistence.impl.function.groupconcat;

import com.blazebit.persistence.impl.function.Order;
import com.blazebit.persistence.spi.FunctionRenderContext;
import com.blazebit.persistence.spi.JpqlFunction;
import com.blazebit.persistence.spi.TemplateRenderer;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author Christian Beikov
 * @since 1.0.0
 */
public abstract class AbstractGroupConcatFunction implements JpqlFunction {

    public static final String FUNCTION_NAME = "group_concat";

    protected final TemplateRenderer renderer;

    public AbstractGroupConcatFunction(String template) {
        this.renderer = new TemplateRenderer(template);
    }

    @Override
    public boolean hasArguments() {
        return true;
    }

    @Override
    public boolean hasParenthesesIfNoArguments() {
        return true;
    }

    @Override
    public Class<?> getReturnType(Class<?> firstArgumentType) {
        return String.class;
    }

    @Override
    public void render(FunctionRenderContext context) {
        render(context, getGroupConcat(context));
    }

    public abstract void render(FunctionRenderContext context, GroupConcat groupConcat);

    protected GroupConcat getGroupConcat(FunctionRenderContext context) {
        if (context.getArgumentsSize() == 0) {
            throw new RuntimeException("The group concat function needs at least one argument! args=" + context);
        }

        boolean distinct = false;
        String expression;
        int startIndex = 0;
        int argsSize = context.getArgumentsSize();
        String maybeDistinct = context.getArgument(0);

        if ("'DISTINCT'".equalsIgnoreCase(maybeDistinct)) {
            distinct = true;
            startIndex++;
        }

        if (startIndex >= argsSize) {
            throw new RuntimeException("The group concat function needs at least one expression to concatenate! args=" + context);
        }

        expression = context.getArgument(startIndex);

        String separator = null;
        String orderExpression = null;
        List<Order> orders = new ArrayList<Order>();
        Mode mode = null;

        for (int i = startIndex + 1; i < argsSize; i++) {
            String argument = context.getArgument(i);
            if ("'SEPARATOR'".equalsIgnoreCase(argument)) {
                mode = Mode.SEPARATOR;
            } else if ("'ORDER BY'".equalsIgnoreCase(argument)) {
                mode = Mode.ORDER_BY;
            } else if ("'WITHIN GROUP'".equalsIgnoreCase(argument)) {
                mode = Mode.ORDER_BY;
            } else {
                if (mode == Mode.ORDER_BY) {
                    Order order = getOrder(argument, orderExpression);
                    if (order != null) {
                        orders.add(order);
                        orderExpression = null;
                    } else {
                        if (orderExpression != null) {
                            orders.add(new Order(orderExpression, null, null));
                        }
                        
                        orderExpression = argument;
                    }
                } else if (mode == Mode.SEPARATOR) {
                    if (separator != null) {
                        throw new IllegalArgumentException("Illegal multiple separators for group concat '" + argument + "'. Expected 'ORDER BY'!");
                    }

                    separator = argument.substring(argument.indexOf('\'') + 1, argument.lastIndexOf('\''));
                } else {
                    separator = argument.substring(argument.indexOf('\'') + 1, argument.lastIndexOf('\''));
                    mode = Mode.SEPARATOR;
                }
            }
        }

        if (orderExpression != null) {
            orders.add(new Order(orderExpression, null, null));
        }

        if (separator == null) {
            separator = ",";
        }

        return new GroupConcat(distinct, expression, orders, separator);
    }

    /**
     * @author Christian Beikov
     * @since 1.2.0
     */
    private enum Mode {
        SEPARATOR,
        ORDER_BY
    }

    protected void render(StringBuilder sb, Order order) {
        sb.append(order.getExpression());
        
        if (order.isAscending()) {
            sb.append(" ASC");
        } else {
            sb.append(" DESC");
        }
        
        if (order.isNullsFirst()) {
            sb.append(" NULLS FIRST");
        } else {
            sb.append(" NULLS LAST");
        }
    }
    
    protected void appendEmulatedOrderByElementWithNulls(StringBuilder sb, Order element) {
        sb.append("case when ");
        sb.append(element.getExpression());
        sb.append(" is null then ");
        sb.append(element.isNullsFirst() ? 0 : 1);
        sb.append(" else ");
        sb.append(element.isNullsFirst() ? 1 : 0);
        sb.append(" end, ");
        sb.append(element.getExpression());
        sb.append(element.isAscending() ? " asc" : " desc");
    }

    private static Order getOrder(String s, String expression) {
        if (expression == null) {
            return null;
        }
        
        String type = s.trim().toUpperCase();
        
        if ("'ASC'".equals(type)) {
            return new Order(expression, true, null);
        } else if ("'DESC'".equals(type)) {
            return new Order(expression, false, null);
        } else if ("'ASC NULLS FIRST'".equals(type)) {
            return new Order(expression, true, true);
        } else if ("'ASC NULLS LAST'".equals(type)) {
            return new Order(expression, true, false);
        } else if ("'DESC NULLS FIRST'".equals(type)) {
            return new Order(expression, false, true);
        } else if ("'DESC NULLS LAST'".equals(type)) {
            return new Order(expression, false, false);
        }
        
        return null;
    }

    /**
     * @author Christian Beikov
     * @since 1.2.0
     */
    public static final class GroupConcat {

        private final boolean distinct;
        private final String expression;
        private final List<Order> orderBys;
        private final String separator;

        public GroupConcat(boolean distinct, String expression, List<Order> orderBys, String separator) {
            this.distinct = distinct;
            this.expression = expression;
            this.orderBys = orderBys;
            this.separator = separator;
        }

        public boolean isDistinct() {
            return distinct;
        }

        public String getExpression() {
            return expression;
        }

        public List<Order> getOrderBys() {
            return orderBys;
        }

        public String getSeparator() {
            return separator;
        }
    }
}
